import Tone from "../core/Tone";
import "../signal/Signal";
import "../signal/Pow";
import "../type/Type";
import "../core/AudioNode";

/**
 *  @class  Tone.Envelope is an [ADSR](https://en.wikipedia.org/wiki/Synthesizer#ADSR_envelope)
 *          envelope generator. Tone.Envelope outputs a signal which
 *          can be connected to an AudioParam or Tone.Signal.
 *          <img src="https://upload.wikimedia.org/wikipedia/commons/e/ea/ADSR_parameter.svg">
 *
 *  @constructor
 *  @extends {Tone.AudioNode}
 *  @param {Time} [attack] The amount of time it takes for the envelope to go from
 *                         0 to it's maximum value.
 *  @param {Time} [decay]	The period of time after the attack that it takes for the envelope
 *                       	to fall to the sustain value. Value must be greater than 0.
 *  @param {NormalRange} [sustain]	The percent of the maximum value that the envelope rests at until
 *                                	the release is triggered.
 *  @param {Time} [release]	The amount of time after the release is triggered it takes to reach 0.
 *                         	Value must be greater than 0.
 *  @example
 * //an amplitude envelope
 * var gainNode = Tone.context.createGain();
 * var env = new Tone.Envelope({
 * 	"attack" : 0.1,
 * 	"decay" : 0.2,
 * 	"sustain" : 1,
 * 	"release" : 0.8,
 * });
 * env.connect(gainNode.gain);
 */
Tone.Envelope = function(){

	//get all of the defaults
	var options = Tone.defaults(arguments, ["attack", "decay", "sustain", "release"], Tone.Envelope);
	Tone.AudioNode.call(this);

	/**
	 *  When triggerAttack is called, the attack time is the amount of
	 *  time it takes for the envelope to reach it's maximum value.
	 *  @type {Time}
	 */
	this.attack = options.attack;

	/**
	 *  After the attack portion of the envelope, the value will fall
	 *  over the duration of the decay time to it's sustain value.
	 *  @type {Time}
	 */
	this.decay = options.decay;

	/**
	 * 	The sustain value is the value
	 * 	which the envelope rests at after triggerAttack is
	 * 	called, but before triggerRelease is invoked.
	 *  @type {NormalRange}
	 */
	this.sustain = options.sustain;

	/**
	 *  After triggerRelease is called, the envelope's
	 *  value will fall to it's miminum value over the
	 *  duration of the release time.
	 *  @type {Time}
	 */
	this.release = options.release;

	/**
	 *  the next time the envelope is at standby
	 *  @type {number}
	 *  @private
	 */
	this._attackCurve = "linear";

	/**
	 *  the next time the envelope is at standby
	 *  @type {number}
	 *  @private
	 */
	this._releaseCurve = "exponential";

	/**
	 *  the signal
	 *  @type {Tone.Signal}
	 *  @private
	 */
	this._sig = this.output = new Tone.Signal(0);

	//set the attackCurve initially
	this.attackCurve = options.attackCurve;
	this.releaseCurve = options.releaseCurve;
	this.decayCurve = options.decayCurve;
};

Tone.extend(Tone.Envelope, Tone.AudioNode);

/**
 *  the default parameters
 *  @static
 *  @const
 */
Tone.Envelope.defaults = {
	"attack" : 0.01,
	"decay" : 0.1,
	"sustain" : 0.5,
	"release" : 1,
	"attackCurve" : "linear",
	"decayCurve" : "exponential",
	"releaseCurve" : "exponential",
};

/**
 * Read the current value of the envelope. Useful for
 * syncronizing visual output to the envelope.
 * @memberOf Tone.Envelope#
 * @type {Number}
 * @name value
 * @readOnly
 */
Object.defineProperty(Tone.Envelope.prototype, "value", {
	get : function(){
		return this.getValueAtTime(this.now());
	}
});

/**
 *  Get the curve
 *  @param  {Array|String}  curve
 *  @param  {String}  direction  In/Out
 *  @return  {String}  The curve name
 *  @private
 */
Tone.Envelope.prototype._getCurve = function(curve, direction){
	if (Tone.isString(curve)){
		return curve;
	} else if (Tone.isArray(curve)){
		//look up the name in the curves array
		for (var t in Tone.Envelope.Type){
			if (Tone.Envelope.Type[t][direction] === curve){
				return t;
			}
		}
	}
};

/**
 *  Assign a the curve to the given name using the direction
 *  @param  {String}  name
 *  @param  {String}  direction In/Out
 *  @param  {Array}  curve
 *  @private
 */
Tone.Envelope.prototype._setCurve = function(name, direction, curve){
	//check if it's a valid type
	if (Tone.Envelope.Type.hasOwnProperty(curve)){
		var curveDef = Tone.Envelope.Type[curve];
		if (Tone.isObject(curveDef)){
			this[name] = curveDef[direction];
		} else {
			this[name] = curveDef;
		}
	} else if (Tone.isArray(curve)){
		this[name] = curve;
	} else {
		throw new Error("Tone.Envelope: invalid curve: " + curve);
	}
};

/**
 * The shape of the attack.
 * Can be any of these strings:
 * <ul>
 *   <li>linear</li>
 *   <li>exponential</li>
 *   <li>sine</li>
 *   <li>cosine</li>
 *   <li>bounce</li>
 *   <li>ripple</li>
 *   <li>step</li>
 * </ul>
 * Can also be an array which describes the curve. Values
 * in the array are evenly subdivided and linearly
 * interpolated over the duration of the attack.
 * @memberOf Tone.Envelope#
 * @type {String|Array}
 * @name attackCurve
 * @example
 * env.attackCurve = "linear";
 * @example
 * //can also be an array
 * env.attackCurve = [0, 0.2, 0.3, 0.4, 1]
 */
Object.defineProperty(Tone.Envelope.prototype, "attackCurve", {
	get : function(){
		return this._getCurve(this._attackCurve, "In");
	},
	set : function(curve){
		this._setCurve("_attackCurve", "In", curve);
	}
});

/**
 * The shape of the release. See the attack curve types.
 * @memberOf Tone.Envelope#
 * @type {String|Array}
 * @name releaseCurve
 * @example
 * env.releaseCurve = "linear";
 */
Object.defineProperty(Tone.Envelope.prototype, "releaseCurve", {
	get : function(){
		return this._getCurve(this._releaseCurve, "Out");
	},
	set : function(curve){
		this._setCurve("_releaseCurve", "Out", curve);
	}
});

/**
 * The shape of the decay either "linear" or "exponential"
 * @memberOf Tone.Envelope#
 * @type {String}
 * @name decayCurve
 * @example
 * env.decayCurve = "linear";
 */
Object.defineProperty(Tone.Envelope.prototype, "decayCurve", {
	get : function(){
		return this._decayCurve;
	},
	set : function(curve){
		var curves = ["linear", "exponential"];
		if (!curves.includes(curve)){
			throw new Error("Tone.Envelope: invalid curve: " + curve);
		} else {
			this._decayCurve = curve;
		}
	}
});

/**
 *  Trigger the attack/decay portion of the ADSR envelope.
 *  @param  {Time} [time=now] When the attack should start.
 *  @param {NormalRange} [velocity=1] The velocity of the envelope scales the vales.
 *                               number between 0-1
 *  @returns {Tone.Envelope} this
 *  @example
 *  //trigger the attack 0.5 seconds from now with a velocity of 0.2
 *  env.triggerAttack("+0.5", 0.2);
 */
Tone.Envelope.prototype.triggerAttack = function(time, velocity){
	this.log("triggerAttack", time, velocity);
	time = this.toSeconds(time);
	var originalAttack = this.toSeconds(this.attack);
	var attack = originalAttack;
	var decay = this.toSeconds(this.decay);
	velocity = Tone.defaultArg(velocity, 1);
	//check if it's not a complete attack
	var currentValue = this.getValueAtTime(time);
	if (currentValue > 0){
		//subtract the current value from the attack time
		var attackRate = 1 / attack;
		var remainingDistance = 1 - currentValue;
		//the attack is now the remaining time
		attack = remainingDistance / attackRate;
	}
	//attack
	if (attack === 0){
		//case where the attack time is 0 should set instantly
		this._sig.setValueAtTime(velocity, time);
	} else if (this._attackCurve === "linear"){
		this._sig.linearRampTo(velocity, attack, time);
	} else if (this._attackCurve === "exponential"){
		this._sig.targetRampTo(velocity, attack, time);
	} else if (attack > 0){
		this._sig.cancelAndHoldAtTime(time);
		var curve = this._attackCurve;
		//find the starting position in the curve
		for (var i = 1; i < curve.length; i++){
			//the starting index is between the two values
			if (curve[i-1] <= currentValue && currentValue <= curve[i]){
				curve = this._attackCurve.slice(i);
				//the first index is the current value
				curve[0] = currentValue;
				break;
			}
		}
		this._sig.setValueCurveAtTime(curve, time, attack, velocity);
	}
	//decay
	if (decay){
		var decayValue = velocity * this.sustain;
		var decayStart = time + attack;
		this.log("decay", decayStart);
		if (this._decayCurve === "linear"){
			this._sig.linearRampTo(decayValue, decay, decayStart+this.sampleTime);
		} else if (this._decayCurve === "exponential"){
			this._sig.exponentialApproachValueAtTime(decayValue, decayStart, decay);
		}
	}
	return this;
};

/**
 *  Triggers the release of the envelope.
 *  @param  {Time} [time=now] When the release portion of the envelope should start.
 *  @returns {Tone.Envelope} this
 *  @example
 *  //trigger release immediately
 *  env.triggerRelease();
 */
Tone.Envelope.prototype.triggerRelease = function(time){
	this.log("triggerRelease", time);
	time = this.toSeconds(time);
	var currentValue = this.getValueAtTime(time);
	if (currentValue > 0){
		var release = this.toSeconds(this.release);
		if (this._releaseCurve === "linear"){
			this._sig.linearRampTo(0, release, time);
		} else if (this._releaseCurve === "exponential"){
			this._sig.targetRampTo(0, release, time);
		} else {
			var curve = this._releaseCurve;
			if (Tone.isArray(curve)){
				this._sig.cancelAndHoldAtTime(time);
				this._sig.setValueCurveAtTime(curve, time, release, currentValue);
			}
		}
	}
	return this;
};

/**
 *  Get the scheduled value at the given time. This will
 *  return the unconverted (raw) value.
 *  @param  {Number}  time  The time in seconds.
 *  @return  {Number}  The scheduled value at the given time.
 */
Tone.Envelope.prototype.getValueAtTime = function(time){
	return this._sig.getValueAtTime(time);
};

/**
 *  triggerAttackRelease is shorthand for triggerAttack, then waiting
 *  some duration, then triggerRelease.
 *  @param {Time} duration The duration of the sustain.
 *  @param {Time} [time=now] When the attack should be triggered.
 *  @param {number} [velocity=1] The velocity of the envelope.
 *  @returns {Tone.Envelope} this
 *  @example
 * //trigger the attack and then the release after 0.6 seconds.
 * env.triggerAttackRelease(0.6);
 */
Tone.Envelope.prototype.triggerAttackRelease = function(duration, time, velocity){
	time = this.toSeconds(time);
	this.triggerAttack(time, velocity);
	this.triggerRelease(time + this.toSeconds(duration));
	return this;
};

/**
 *  Cancels all scheduled envelope changes after the given time.
 *  @param  {Time} after
 *  @returns {Tone.Envelope} this
 */
Tone.Envelope.prototype.cancel = function(after){
	this._sig.cancelScheduledValues(after);
	return this;
};

/**
 *  Borrows the connect method from Tone.Signal.
 *  @function
 *  @private
 */
Tone.Envelope.prototype.connect = Tone.SignalBase.prototype.connect;

/**
 	 *  Generate some complex envelope curves.
 	 */
(function _createCurves(){

	var curveLen = 128;

	var i, k;

	//cosine curve
	var cosineCurve = [];
	for (i = 0; i < curveLen; i++){
		cosineCurve[i] = Math.sin((i / (curveLen - 1)) * (Math.PI / 2));
	}

	//ripple curve
	var rippleCurve = [];
	var rippleCurveFreq = 6.4;
	for (i = 0; i < curveLen - 1; i++){
		k = (i / (curveLen - 1));
		var sineWave = Math.sin(k * (Math.PI * 2) * rippleCurveFreq - Math.PI / 2) + 1;
		rippleCurve[i] = sineWave/10 + k * 0.83;
	}
	rippleCurve[curveLen - 1] = 1;

	//stairs curve
	var stairsCurve = [];
	var steps = 5;
	for (i = 0; i < curveLen; i++){
		stairsCurve[i] = Math.ceil((i / (curveLen - 1)) * steps) / steps;
	}

	//in-out easing curve
	var sineCurve = [];
	for (i = 0; i < curveLen; i++){
		k = i / (curveLen - 1);
		sineCurve[i] = 0.5 * (1 - Math.cos(Math.PI * k));
	}

	//a bounce curve
	var bounceCurve = [];
	for (i = 0; i < curveLen; i++){
		k = i / (curveLen - 1);
		var freq = Math.pow(k, 3) * 4 + 0.2;
		var val = Math.cos(freq * Math.PI * 2 * k);
		bounceCurve[i] = Math.abs(val * (1 - k));
	}

	/**
	 *  Invert a value curve to make it work for the release
	 *  @private
	 */
	function invertCurve(curve){
		var out = new Array(curve.length);
		for (var j = 0; j < curve.length; j++){
			out[j] = 1 - curve[j];
		}
		return out;
	}

	/**
	 *  reverse the curve
	 *  @private
	 */
	function reverseCurve(curve){
		return curve.slice(0).reverse();
	}

	/**
	 *  attack and release curve arrays
	 *  @type  {Object}
	 *  @private
	 */
	Tone.Envelope.Type = {
		"linear" : "linear",
		"exponential" : "exponential",
		"bounce" : {
			In : invertCurve(bounceCurve),
			Out : bounceCurve
		},
		"cosine" : {
			In : cosineCurve,
			Out : reverseCurve(cosineCurve)
		},
		"step" : {
			In : stairsCurve,
			Out : invertCurve(stairsCurve)
		},
		"ripple" : {
			In : rippleCurve,
			Out : invertCurve(rippleCurve)
		},
		"sine" : {
			In : sineCurve,
			Out : invertCurve(sineCurve)
		}
	};

})();

/**
 *  Disconnect and dispose.
 *  @returns {Tone.Envelope} this
 */
Tone.Envelope.prototype.dispose = function(){
	Tone.AudioNode.prototype.dispose.call(this);
	this._sig.dispose();
	this._sig = null;
	this._attackCurve = null;
	this._releaseCurve = null;
	return this;
};

export default Tone.Envelope;

